/*
 * Copyright 2006-2007 S W McIlroy
 * Licensed  under the  Apache License,  Version 2.0  (the "License");
 * you may not use  this file  except in  compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed  under the  License is distributed on an "AS IS" BASIS,
 * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
 * implied.
 *
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.mockfu;

import static org.mockfu.Exceptions.*;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * An expectation defines a method invocation pattern that is anticipated to be encountered for a
 * mock object under test. Its pattern includes the method signature, parameter constraints, and
 * number of invocations.
 * <p>
 * An expectation may optionally define a stub, which defines the behavior and return value for the
 * method on the mock when the expectation is matched.
 */
class Expectation implements Expectable
{

    private final Multiplicity multiplicity;
    private final List<Matcher<Object>> matchers = new ArrayList<Matcher<Object>>();
    /* guarded by instance */
    private Method method;
    private int timesMatched = 0;
    private volatile Stub<?> stub;

    Expectation()
    {
        multiplicity = new Multiplicity( 1, 1 );
    }

    Expectation( Multiplicity multiplicity )
    {
        this.multiplicity = multiplicity;
    }

    synchronized void configure( Method method, List<Matcher<Object>> matchers )
    {
        this.method = method;
        this.matchers.clear();
        this.matchers.addAll( matchers );
    }

    public boolean isAllows()
    {
        return multiplicity.isAllows();
    }

    public Object match( InvocationContext context )
    {
        Object result = NO_MATCH;

        if( matches( context ) )
        {
            result = null;
            Stub<?> stub = this.stub;
            if( stub != null )
            {
                /*
                 * always execute stub if expectation matches, even if multiplicity exceeded, since
                 * stub may cancel the match
                 */
                context.setExpectation( this );
                try
                {
                    /* for the sake of liveness do not synchronize */
                    result = stub.invoke( context );
                }
                catch( UnmatchedException ex )
                {
                    return NO_MATCH;
                }
                catch( Throwable ex )
                {
                    if( context.isIncrementable() )
                        increment();
                    throw undeclared( ex );
                }
            }
            if( context.isIncrementable() )
                increment();
        }
        return result;
    }

    private synchronized boolean matches( InvocationContext context )
    {
        if( !this.method.equals( context.getMethod() ) )
            return false;

        Object[] arguments = context.getArguments();
        if( arguments.length != matchers.size() )
            throw new IllegalStateException( "Number of arguments"
                    + " does not match number of constraints" );

        /*
         * temporarily disable expectation increments so that mock arguments can be interrogated by
         * matchers without affecting the expectations for that mock
         */
        boolean previousIncrementable = InvocationContext.setIncrementable( false );
        try
        {
            for( int i = 0 ; i < arguments.length ; i++ )
                if( !matchers.get( i ).matches( arguments[i] ) )
                    return false;
        }
        finally
        {
            InvocationContext.setIncrementable( previousIncrementable );
        }

        return true;
    }

    private synchronized void increment()
    {
        timesMatched++;

        if( multiplicity.isExceeded( timesMatched ) )
            throw new ExpectationFailure( this );
    }

    public synchronized int getMatchCount()
    {
        return timesMatched;
    }

    public synchronized boolean stubWithIfPossible( Stub<?> stub, MethodFilter filter )
    {
        if( this.stub == null )
        {
            if( filter == null || filter.matches( method ) )
            {
                this.stub = stub;
                return true;
            }
        }
        return false;
    }

    @Override
    public synchronized String toString()
    {
        StringBuilder s = new StringBuilder();
        s.append( method.getDeclaringClass().getSimpleName() );
        s.append( "." ).append( method.getName() ).append( "(" );

        for( Iterator<Matcher<Object>> i = matchers.iterator() ; i.hasNext() ; )
        {
            s.append( i.next() );
            if( i.hasNext() )
                s.append( ", " );
        }

        s.append( ") " );
        s.append( multiplicity );
        return s.toString();
    }

    public synchronized boolean canMatchMore()
    {
        return !multiplicity.isExceeded( timesMatched + 1 );
    }

    public synchronized boolean verifies()
    {
        return multiplicity.contains( timesMatched );
    }

    public synchronized void verify()
    {
        if( !multiplicity.contains( timesMatched ) )
            throw new ExpectationFailure( this );
    }

    public synchronized void reset()
    {
        timesMatched = 0;
    }

    @Override
    public int hashCode()
    {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((matchers == null) ? 0 : matchers.hashCode());
        result = prime * result + ((method == null) ? 0 : method.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj)
    {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        final Expectation other = (Expectation)obj;
        if (matchers == null)
        {
            if (other.matchers != null)
                return false;
        }
        else if (!matchers.equals(other.matchers))
            return false;
        if (method == null)
        {
            if (other.method != null)
                return false;
        }
        else if (!method.equals(other.method))
            return false;
        /* "allows" expectations are never duplicate: any number can be defined for a single method */
        return !isAllows() && !other.isAllows();
    }

}
